﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Linq;

namespace XamlFilter
{
    class Generator
    {
        private readonly XmlDocument _template;
        private readonly Dictionary<string, string> _templateNamespaces = new Dictionary<string, string>();
        private HashSet<string> _conditions = new HashSet<string>();
        private readonly string _templateFolder;
             
        public Generator(string template)
        {
            _templateFolder = Path.GetDirectoryName(template);

            var reader = XmlReader.Create(template, new XmlReaderSettings {ValidationType = ValidationType.None});
            _template = new XmlDocument();
            _template.Load(reader);

            CacheNamespaces(_template, _templateNamespaces);
        }

        public IEnumerable<string> Conditions
        {
            get { return this._conditions; }
            set { this._conditions = new HashSet<string>(value); }
        }
        public bool Indent { get; set; }

        public void Generate(string output)
        {
            var doc = new XmlDocument();

            InitDoc(doc);

            ConvertElement(doc, _template.DocumentElement, doc.DocumentElement);

            EnsureFolder(Path.GetDirectoryName(output));
            var writer = XmlWriter.Create(output, new XmlWriterSettings { Indent = Indent, IndentChars = "  " });
            doc.Save(writer);

            writer.Flush();
            writer.Close();
        }

        private static void EnsureFolder(string path)
        {
            if (Directory.Exists(path))
                return;

            //EnsureFolder(Path.GetDirectoryName(path));
            Directory.CreateDirectory(path);
        }

        private void InitDoc(XmlDocument doc)
        {
            foreach (XmlNode node in _template.ChildNodes)
            {
                if (node.NodeType == XmlNodeType.ProcessingInstruction||
                    node.NodeType == XmlNodeType.Comment ||
                    node.NodeType == XmlNodeType.Whitespace ||
                    node.NodeType == XmlNodeType.SignificantWhitespace)
                {
                    doc.AppendChild(node);
                }
                else if (node.NodeType == XmlNodeType.Element)
                {
                    doc.AppendChild(doc.CreateElement(node.Prefix, node.Name, node.NamespaceURI));
                }
            }

            UpdateNamespaces(doc);
        }

        private static void CacheNamespaces(XmlDocument document, Dictionary<string, string> output)
        {
            foreach (XmlAttribute attr in document.DocumentElement.Attributes)
            {
                if (attr.Prefix == Constants.XmlnsLabel || attr.LocalName == Constants.XmlnsLabel)
                {
                    var name = attr.LocalName;
                    if (name == Constants.XmlnsLabel)
                    {
                        name = "";
                    }
                    var value = attr.Value;
                    value = value.Split('#')[0]; // 将url中#后的内容丢弃。
                    output.Add(name, value);
                }
            }
        }

        private void UpdateNamespaces(XmlDocument doc)
        {
            foreach (var pair in _templateNamespaces)
            {
                if (pair.Key.Contains(Constants.ConditionSuffix))
                    continue;

                ConvertNamespaceAttributeTo(doc, pair.Key, pair.Value);
            }
        }
        private void ConvertNamespaceAttributeTo(XmlDocument doc, string prefix, string ns)
        {
            var existedPrefix = doc.DocumentElement.GetNamespaceOfPrefix(ns);
            if (string.IsNullOrEmpty(existedPrefix))
            {
                if (string.IsNullOrEmpty(prefix))
                    doc.DocumentElement.SetAttribute(Constants.XmlnsLabel, ns);
                else
                    doc.DocumentElement.SetAttribute("xmlns:" + prefix, ns);
            }
            else if (existedPrefix != prefix)
            {
                Console.WriteLine(string.Format("[Warning] Namespace declection conflicted '{0}': {1}<->{2}",
                                                ns, prefix, existedPrefix));
            }
        }

        private void ConvertElement(XmlDocument doc, XmlElement template, XmlElement elem)
        {
            foreach (XmlAttribute attrTemplate in template.Attributes)
            {
                if (attrTemplate.Prefix == Constants.XmlnsLabel || attrTemplate.LocalName == Constants.XmlnsLabel)
                {
                    if (template != _template.DocumentElement)
                        throw new FormatException("Namespace should be defined on root element.");
                    continue;
                }
                if (IsReserved(attrTemplate.Prefix))
                {
                    var ns = GetConvertedNamespace(attrTemplate.Prefix);
                    // 我要从当前文档上去找Ns对应的前缀，就会找不到。因此只能从模板节点上去找。
                    var prefix = attrTemplate.GetPrefixOfNamespace(ns);
                    // 对于Attribute，如果它属于默认命名空间，那么构造的时候不能传入命名空间。否则会
                    // 会自动为这个命名空间分配一个新的前缀。
                    // 但是，如果它属于其它命名空间，那么必须传入命名空间。否则前缀会被丢弃。即使在
                    // 父节点中已经映射了前缀和命名空间。
                    // 原因不详，经过多次尝试，发现下面的组合结果正确。Element的处理和Attribute不同。
                    var attr = string.IsNullOrEmpty(prefix) 
                        ? doc.CreateAttribute(attrTemplate.LocalName)
                        : doc.CreateAttribute(prefix + ":" + attrTemplate.LocalName, ns);
                    attr.Value = attrTemplate.Value;
                    elem.SetAttributeNode(attr);
                }
            }

            ConvertElementContent(doc, template, elem);
        }

        private void ConvertElementContent(XmlDocument doc, XmlElement template, XmlElement elem)
        {
            foreach (XmlNode nodeTemplate in template.ChildNodes)
            {
                var elemTemplate = nodeTemplate as XmlElement;
                if (elemTemplate != null)
                {
                    if (IsReserved(elemTemplate.Prefix))
                    {
                        string ns;
                        var qualifiedName = GetConvertedQualifiedName(elemTemplate, out ns);
                        // 构造时，必须传入ns，否则，保存时会蹦。
                        var childElem = doc.CreateElement(qualifiedName, ns);
                        elem.AppendChild(childElem);
                        this.ConvertElement(doc, elemTemplate, childElem);
                    }
                }
                var commTemplate = nodeTemplate as XmlComment;
                if (commTemplate != null)
                {
                    string comment = commTemplate.Value.Trim();
                    if (comment.StartsWith(Constants.DevCommentMark) && comment.EndsWith(Constants.DevCommentMark))
                    {
                        // ignore dev comments
                    }
                    else if (comment.StartsWith(Constants.PreprocessMark) && comment.EndsWith(Constants.PreprocessMark))
                    {
                        Preprocess(doc, comment.Substring(1, comment.Length - 2).Trim(), elem);
                    }
                    else
                    {
                        elem.AppendChild(doc.CreateComment(comment));
                    }
                }
                var textTemplate = nodeTemplate as XmlText;
                if (textTemplate != null)
                {
                    elem.AppendChild(doc.CreateTextNode(textTemplate.Value));
                }
            }
        }

        private string GetConvertedQualifiedName(XmlNode nodeTemplate, out string ns)
        {
            ns = GetConvertedNamespace(nodeTemplate.Prefix);
            // 我要从当前文档上去找Ns对应的前缀，就会找不到。因此只能从模板节点上去找。
            var prefix = nodeTemplate.GetPrefixOfNamespace(ns);
            return string.IsNullOrEmpty(prefix)
                           ? nodeTemplate.LocalName
                           : prefix + ":" + nodeTemplate.LocalName;
        }
        private bool IsReserved(string prefix)
        {
            if (string.IsNullOrEmpty(prefix))
            {
                return true;
            }

            var prefixItems = prefix.Split(new[] {Constants.ConditionSuffix}, StringSplitOptions.None).ToList();
            if (prefixItems.Count == 1)
            {
                return true;
            }
            prefixItems.RemoveAt(prefixItems.Count - 1); // 删除最后一个项目，这个项目不是预定义。

            foreach (var prefixItem in prefixItems)
            {
                var name = prefixItem.Trim();
                if (string.IsNullOrEmpty(name))
                    continue;

                if (!_conditions.Contains(name))
                {
                    return false;
                }
            }
            return true;
        }
        private string GetConvertedNamespace(string prefix)
        {
            return _templateNamespaces[prefix];
        }

        private void Preprocess(XmlDocument doc, string preprocessSentence, XmlElement elem)
        {
            var command = preprocessSentence.Substring(0, preprocessSentence.IndexOf(' ')).ToLower();
            switch (command)
            {
                case Constants.PC_Include:
                    PreprocessInclude(doc, preprocessSentence.Substring(preprocessSentence.IndexOf(' ')).Trim().ToLower(), elem);
                    return;
            }
        }
        private void PreprocessInclude(XmlDocument doc, string path, XmlElement elem)
        {
            if (!Path.IsPathRooted(path))
                path = Path.Combine(_templateFolder, path);

            if (!File.Exists(path))
                throw new ArgumentException(
                    string.Format("Include directive error, cannot found the specified template part: '{0}'",
                                  path));

            var partGenerator = new Generator(path);
            partGenerator.Conditions = this.Conditions;
            partGenerator.UpdateNamespaces(doc);
            partGenerator.ConvertElementContent(doc, partGenerator._template.DocumentElement, elem);
        }
    }
    static class Constants
    {
        public const string XmlnsLabel = "xmlns";
        public const string ConditionSuffix = "_";
        public const string DevCommentMark = "#";
        public const string PreprocessMark = "@";
        public const string PC_Include = "include";
    }
}
